stm32cubeMX学习、USB DFU(Download Firmware Update)固件更新
The following article is from 嵌入式云IOT技术圈 Author 杨源鑫
本程序编写基于秉火霸道STM32F103ZET6运行环境。
最近疫情期间,特地将自己大部分硬件资源全部用热胶抢焊到了一起,以便以后自己复习和学习,当然还有很多,弄不上来了,只能等以后有机会再重新搞一块!我还是非常舍得花钱买设备的!哈哈!这是一个STM32+Linux+51的大杂烩开发平台!
1、产生问题
公司的产品,每次生产烧写程序都得把机器拆开,然后插上串行线或者ST-Link进行烧写,产品量产的情况下数量很多,所以生产每次都需要花费很长去时间去给机器烧程序(这里我们用野火的开发板来模拟)。
2、现有的硬件接口
现在的产品(野火的STM32F103ZET6开发板)有一个USB接口,硬件连接图如下:
如上图所示,当PD3为低电平的时候,USB接口供电,即可用,这一点在上一篇文章已经讲解了,我们在STM32CubeMX把这个管脚默认拉低即可。
3、分析问题
STM32CubeMX支持了与USB相关的诸多配置功能,请看如下:
于我们需要使用USB接口来更新程序,所以我们需要在配置USB设备模式的时候给它选择Download Firmware Update Class(DFU)。
1、USB烧写原理及流程分析
1.1 烧写原理
这点与IAP升级是大同小异的,只不过这里我们使用了USB来烧写,之前写过类似的一篇文章:带串口屏显示的BootLoader程序开发 在这篇文章里面也介绍了相应的原理,这里就不再重复描述,我们负责把这篇文章里提到的几点实现就可以了。
1.2 程序存储分区
STM32F103ZET6的FLASH容量一共有512KB。所以,我给BootLoader的大小是64K,也就是0x10000,具体是怎么算的呢?
0x10000转十进制为65536,65536/1024 = 64K
把剩下的空间全部分配给APP,也就是0x70000,具体是怎么算的呢?
0x70000转十进制为458752,458752/1024 = 448K
4、解决问题
4.1 配置编写BootLoader程序的CubeMX工程
4.1.1 配置RCC时钟
4.1.2 配置串行调试接口
4.1.3 配置按键、调试灯、调试串口、USB使能管脚
调试灯选择的是PB1,低电平点亮,具体可以看原理图:
USB使能管脚默认为低电平。
选用USART2作为调试打印输出。
4.1.4 配置USB相关的选项
配置的基本参数默认即可,不需要改变。
在中断设置这里,将USB优先级调低,可以避免一些默认其妙不稳定的现象。接下来配置USB设备相关的选项。
类参数有一个字段比较重要:
@Internal Flash /0x08000000/03*016Ka,01*016Kg,01*064Kg,07*128Kg,04*016Kg,01*064Kg,07*128Kg
这个参数的具体含义描述如下:
@:检测到这是一个特殊的映射描述符(避免解码标准描述符)
/:用于区域之间的分隔符
每个地址以“ 0x”开头的最大8位数字
/:用于区域之间的分隔符
扇区数的最大2位数字
*:用于扇区数和扇区大小之间的分隔符
扇区大小在0到999之间的最大3位
扇区大小乘数的1位数字。有效条目为:B(字节),K(千),M(兆)
扇区类型的1位数字,如下所示:
– a(0x41):可读
– b(0x42):可擦除
– c(0x43):可读和可擦除
(0x44):可写
– e(0x45):可读写
–f(0x46):可擦除和可写
–g(0x47):可读写,可写
4.1.5 生成工程
这里默认不让它自动生成main函数,main函数我们自己写。在配置USB设备参数里,USBD_DFU_XFER_SIZE参数:USB数据pack大小,越大配置速度越快。默认配置1024Bytes. 1024Bytes使用的是堆空间,故堆空间要大于1024Bytes. 原因:代码如下。
#define USBD_malloc malloc
/* Allocate Audio structure */
pdev->pClassData = USBD_malloc(sizeof (USBD_DFU_HandleTypeDef));
所以这里的堆我把它配置成0x1000。(个人习惯)
4.2 编写BootLoader程序
4.2.1 实现usbd_dfu_if.c中相关的接口
宏定义一些参数
//FLASH的擦写实现
#define FLASH_ERASE_TIME (uint16_t)50
#define FLASH_PROGRAM_TIME (uint16_t)50
//APP存放的结束地址
#define USBD_DFU_APP_END_ADD 0x08080000
//FLASH页大小
#define FLASH_PAGE_SIZE 0x800U //2K
实现如下接口:
MEM_If_Init_FS, 闪存初始化,解锁内部flash。
MEM_If_DeInit_FS, 闪存反(取消)初始化,上锁内部flash。
MEM_If_Erase_FS, 闪存擦除。
MEM_If_Write_FS, 闪存写入。
MEM_If_Read_FS, 闪存读取。
MEM_If_GetStatus_FS 获取闪存状态,返回写入或擦除操作所需的时间。
闪存初始化,解锁内部flash。
uint16_t MEM_If_Init_FS(void)
{
/* USER CODE BEGIN 0 */
//解锁内部FLASH
HAL_FLASH_Unlock();
//清除FLASH的一些标志,可以避免一些莫名其妙的问题
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_WRPERR | FLASH_FLAG_PGERR);
return (USBD_OK);
/* USER CODE END 0 */
}
闪存反(取消)初始化,上锁内部flash。
uint16_t MEM_If_DeInit_FS(void)
{
/* USER CODE BEGIN 1 */
//给FLASH上锁
HAL_FLASH_Lock();
return (USBD_OK);
/* USER CODE END 1 */
}
闪存擦除。
uint16_t MEM_If_Erase_FS(uint32_t Add)
{
/* USER CODE BEGIN 2 */
/*擦除整个APP程序存放的空间,即是0x08080000-0x08010000*/
/*
因为起始地址是0x8000000,而Size是0x80000,所以MCU存放代码的最后一个区域的地址为0x8080000。
而DFU占了其中的0x10000的空间。
*/
uint32_t NbOfPages = 0 ;
uint32_t PageError = 0 ;
FLASH_EraseInitTypeDef pEraseInit ;
NbOfPages = (USBD_DFU_APP_END_ADD - USBD_DFU_APP_DEFAULT_ADD)/FLASH_PAGE_SIZE ;
pEraseInit.TypeErase = FLASH_TYPEERASE_PAGES;
pEraseInit.PageAddress = USBD_DFU_APP_DEFAULT_ADD;
pEraseInit.NbPages = NbOfPages; //erase all pages of APP
if(HAL_FLASHEx_Erase(&pEraseInit,&PageError)!= HAL_OK)
return USBD_FAIL ;
return (USBD_OK);
/* USER CODE END 2 */
}
闪存写入。
uint16_t MEM_If_Write_FS(uint8_t *src, uint8_t *dest, uint32_t Len)
{
/* USER CODE BEGIN 3 */
uint32_t i =0;
for(i=0;i<Len;i+=4)
{
if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,(uint32_t)(dest+i),*(uint32_t*)(src+i))== HAL_OK)
{
if(*(uint32_t*)(src+i) != *(uint32_t*)(dest+i))
return USBD_FAIL;
}
else
{
return USBD_FAIL;
}
}
return (USBD_OK);
/* USER CODE END 3 */
}
闪存读取
uint8_t *MEM_If_Read_FS(uint8_t *src, uint8_t *dest, uint32_t Len)
{
/* Return a valid address to avoid HardFault */
/* USER CODE BEGIN 4 */
uint32_t i = 0;
uint8_t *psrc = src;
for (i = 0; i < Len; i++)
{
dest[i] = *psrc++;
}
return (uint8_t*) (dest);
/* USER CODE END 4 */
}
获取闪存状态,返回写入或擦除操作所需的时间。
uint16_t MEM_If_GetStatus_FS(uint32_t Add, uint8_t Cmd, uint8_t *buffer)
{
/* USER CODE BEGIN 5 */
switch (Cmd)
{
case DFU_MEDIA_PROGRAM:
buffer[1] = (uint8_t)FLASH_PROGRAM_TIME;
buffer[2] = (uint8_t)(FLASH_PROGRAM_TIME << 8);
buffer[3] = 0;
break;
case DFU_MEDIA_ERASE:
buffer[1] = (uint8_t)FLASH_ERASE_TIME;
buffer[2] = (uint8_t)(FLASH_ERASE_TIME << 8);
buffer[3] = 0;
break ;
default:
break;
}
return (USBD_OK);
/* USER CODE END 5 */
}
4.2.1 实现main.c
定义调试打印接口,这里我用的是USART2
int fputc(int ch, FILE* FILE)
{
HAL_UART_Transmit(&huart2, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
return ch;
}
跳转到APP的代码实现:
static void JumpToApp(void)
{
typedef void (*pFunction)(void);
static pFunction JumpToApplication;
static uint32_t JumpAddress;
/* Test if user code is programmed starting from USBD_DFU_APP_DEFAULT_ADD * address */
if (((*(__IO uint32_t *) USBD_DFU_APP_DEFAULT_ADD) & 0x2FFE0000) == 0x20000000)
{
/* Jump to user application */
JumpAddress = *(__IO uint32_t *) (USBD_DFU_APP_DEFAULT_ADD + 4);
JumpToApplication = (pFunction) JumpAddress;
/* Initialize user application's Stack Pointer */
__set_MSP((*(__IO uint32_t *) USBD_DFU_APP_DEFAULT_ADD));
JumpToApplication();
}
}
在正常启动过程中,如果APP区域存放有数据,我们不希望去启动USB,在刚开始的时候我们可以把USB的功能给失能掉,如果检测到APP区域没有数据,则再初始化USB功能,所以在这里编写一个USB的失能函数。
static void USB_GPIO_DeInit(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11 | GPIO_PIN_12, GPIO_PIN_RESET);
/*Configure GPIO pin*/
GPIO_InitStruct.Pin = GPIO_PIN_11 | GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_Delay(500);
}
main函数实现
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
USB_GPIO_DeInit();
MX_USART2_UART_Init();
/*如果没有按下按键,则自动跳转到APP区,如果跳转不过去,则代表区域无APP*/
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) != GPIO_PIN_SET)
{
JumpToApp();
printf("跳转失败,开始进入DFU模式\r\n");
}
//进入DFU模式
MX_USB_DEVICE_Init();
printf("Bruce.Yang DFU\n");
//调试灯常亮,代表此时在DFU模式
HAL_GPIO_WritePin(LED_BLUE_GPIO_Port, LED_BLUE_Pin,GPIO_PIN_RESET);
while(1)
{
HAL_Delay(1000);
}
}
实现完毕,接下来可以编译程序,下载到开发板,由于没有APP,所以开发板上PB1的灯常亮。
4.2.2 编写APP程序
APP程序很简单,就让PB1灯以500ms的频率进行翻转吧。
配置过程(略)太简单了,应用APP的核心代码如下:
while (1)
{
/* USER CODE END WHILE */
HAL_GPIO_TogglePin(BLUE_LED_GPIO_Port,BLUE_LED_Pin);
HAL_Delay(500);
/* USER CODE BEGIN 3 */
}
接下来主要是在工程里做一些设置。
1、点击魔术棒设置APP启动的地址
2、更改中断向量表偏移
接下来编译生成APP_TEST.hex文件,我们用一个工具来将它烧写到板子上。
安装DFU烧录软件:DfuSe_Demo
官网下载链接:
https://www.st.com/content/st_com/en/products/development-tools/software-development-tools/stm32-software-development-tools/stm32-programmers/stsw-stm32080.html#resource
默认安装即可。
安装成功后得到两个软件。
Dfu file manager是把bin文件或者hex文件生成 .dfu后缀的文件, .dfu后缀的文件就是我们的固件。DfuSe_Demo是烧录 文件后缀 .dfu 软件。
烧录步骤:
1、将.hex文件转化成.dfu后缀的文件
生成后可以看到效果:
2、连接USB到开发板的设备端口到PC
看到没有识别DFU
我们需要手动给它更新下驱动程序,直接就是刚刚下载的DfuSe安装的目录下找对应系统版本的驱动就好了。
最后可以看到该模式被识别了:
接下来打开DfuSeDemo这个软件,可以看到开发板现在已经被识别了。
接下来将刚刚生成的APP_TEST1.dfu加载进来。
点击Upgrade进行升级。
升级成功!接下来点击Leave DFU mode,程序则会自动开始执行。
这时候APP已经跑起来了,灯在以500ms的频率不断闪烁。
至此USB DFU固件成功!
Bootloader代码以及APP代码在这里下载:
链接:https://pan.baidu.com/s/1zRv7j4E8SXgCV5F6RbSo1Q
提取码:5539
如果有兴趣的话,还可以把我之前写的串口屏BootLoader那个程序继续升级一下!
往期精彩
【Linux笔记】pc机_开发板_ubuntu互ping实验
基于STM32分析栈、堆、全局区、常量区、代码区、RAM、ROM